﻿using System;
using System.Collections.Generic;

using Vibstudio.X4NET.IO.INI.Model;
using Vibstudio.X4NET.IO.INI.Model.Configuration;

namespace Vibstudio.X4NET.IO.INI
{
    public class IniDataParser
    {
        #region Initialization

        public IniDataParser()
            : this(new DefaultIniParserConfiguration())
        { }

        public IniDataParser(IIniParserConfiguration parserConfiguration)
        {
            if (parserConfiguration == null)
                throw new ArgumentNullException("parserConfiguration");

            Configuration = parserConfiguration;
        }

        #endregion

        #region State

        public IIniParserConfiguration Configuration { get; private set; }

		#endregion

		#region Operations

        public IniData Parse(string iniDataString)
        {
            IniData iniData = new IniData();
            iniData.Configuration = Configuration.Clone();

            if (string.IsNullOrEmpty(iniDataString))
            {
                return iniData;
            }

            _currentCommentListTemp.Clear();
            _currentSectionNameTemp = null;

            try
            {
                var lines = iniDataString.Split(Environment.NewLine.ToCharArray(), StringSplitOptions.RemoveEmptyEntries);
                foreach (var line in lines)
                {
                    ProcessLine(line, iniData);
                }

                if (_currentCommentListTemp.Count > 0)
                {
                    iniData.Sections.GetSectionData(_currentSectionNameTemp).TrailingComments
                        .AddRange(_currentCommentListTemp);
                    _currentCommentListTemp.Clear();
                }

            }
            catch
            {
                if (Configuration.ThrowExceptionsOnError)
                    throw;

                return null;
            }

            return (IniData)iniData.Clone();
        }

        #endregion

        #region Template Method Design Pattern 

        protected virtual bool LineContainsAComment(string line)
        {
            return !string.IsNullOrEmpty(line) && Configuration.CommentRegex.Match(line).Success;
        }

        protected virtual bool LineMatchesASection(string line)
        {
            return !string.IsNullOrEmpty(line) && Configuration.SectionRegex.Match(line).Success;
        }

        protected virtual bool LineMatchesAKeyValuePair(string line)
        {
            return !string.IsNullOrEmpty(line) && line.Contains(Configuration.KeyValueAssigmentChar.ToString());
        }

        protected virtual string ExtractComment(string line)
        {
            string comment = Configuration.CommentRegex.Match(line).Value.Trim();

            _currentCommentListTemp.Add(comment.Substring(1, comment.Length - 1));

            return line.Replace(comment, "").Trim();
        }

        protected virtual void ProcessLine(string currentLine, IniData currentIniData)
        {
            currentLine = currentLine.Trim();

            if (LineContainsAComment(currentLine))
            {

                currentLine = ExtractComment(currentLine);
            }

            if (currentLine == String.Empty)
                return;

            if (LineMatchesASection(currentLine))
            {
                ProcessSection(currentLine, currentIniData);
                return;
            }

            if (LineMatchesAKeyValuePair(currentLine))
            {
                ProcessKeyValuePair(currentLine, currentIniData);
                return;
            }

            if (Configuration.SkipInvalidLines) return;

            throw new ParsingException(string.Format("Unknown file format. Couldn't parse the line: '{0}'.", currentLine));
        }

        protected virtual void ProcessSection(string line, IniData currentIniData)
        {
            string sectionName = Configuration.SectionRegex.Match(line).Value.Trim();

            sectionName = sectionName.Substring(1, sectionName.Length - 2).Trim();

            if (sectionName == string.Empty)
            {
                throw new ParsingException("Section name is empty");
            }

            _currentSectionNameTemp = sectionName;

            if (currentIniData.Sections.ContainsSection(sectionName))
            {
                if (Configuration.AllowDuplicateSections)
                {
                    return;
                }

                throw new ParsingException(string.Format("Duplicate section with name '{0}' on line '{1}'", sectionName, line));
            }


            currentIniData.Sections.AddSection(sectionName);

            currentIniData.Sections.GetSectionData(sectionName).LeadingComments = _currentCommentListTemp;
            _currentCommentListTemp.Clear();

        }

        protected virtual void ProcessKeyValuePair(string line, IniData currentIniData)
        {
            string key = ExtractKey(line);
            string value = ExtractValue(line);

            if (string.IsNullOrEmpty(_currentSectionNameTemp))
            {
                if (!Configuration.AllowKeysWithoutSection)
                {
                    throw new ParsingException("key value pairs must be enclosed in a section");
                }

                AddKeyToKeyValueCollection(key, value, currentIniData.Global, "global");
            }
            else
            {
                var currentSection = currentIniData.Sections.GetSectionData(_currentSectionNameTemp);

                AddKeyToKeyValueCollection(key, value, currentSection.Keys, _currentSectionNameTemp);
            }
        }

        protected virtual string ExtractKey(string s)
        {
            int index = s.IndexOf(Configuration.KeyValueAssigmentChar, 0);

            return s.Substring(0, index).Trim();
        }

        protected virtual string ExtractValue(string s)
        {
            int index = s.IndexOf(Configuration.KeyValueAssigmentChar, 0);

            return s.Substring(index + 1, s.Length - index - 1).Trim();
        }

        #endregion

        #region Helpers

        private void AddKeyToKeyValueCollection(string key, string value, KeyDataCollection keyDataCollection, string sectionName)
        {
            if (keyDataCollection.ContainsKey(key))
            {
                if (!Configuration.AllowDuplicateKeys)
                {
                    throw new ParsingException(string.Format("Duplicated key '{0}' found in section '{1}", key, sectionName));
                }

                if(Configuration.OverrideDuplicateKeys)
                {
                    keyDataCollection[key] = value;
                }
            }
            else
            {
                keyDataCollection.AddKey(key, value);
            }

            keyDataCollection.GetKeyData(key).Comments = _currentCommentListTemp;
            _currentCommentListTemp.Clear();
        }

        #endregion

        #region Fields

        private readonly List<string> _currentCommentListTemp = new List<string>();

        private string _currentSectionNameTemp;

        #endregion
    }
}
